package simplescenegraph;

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;

import javax.media.opengl.GL;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLContext;
import javax.media.opengl.GLDrawable;
import javax.media.opengl.GLDrawableFactory;
import javax.media.opengl.glu.GLU;
import javax.media.opengl.glu.GLUquadric;
import javax.swing.JFrame;
import javax.swing.JPanel;

import simplescenegraph.assets.Texture;
import simplescenegraph.assets.TextureLoader;
import simplescenegraph.math.AngleAxis3f;
import simplescenegraph.math.Transformable;
import simplescenegraph.math.Vector2f;
import simplescenegraph.math.Vector3f;
import simplescenegraph.math.Vector4f;
import simplescenegraph.math.Vertex3f;

import com.sun.opengl.util.GLUT;

public class GLRenderer extends AbstractRenderer {
	
	private int viewportWidth = 0;
	
	private int viewportHeight = 0;
	
	private GLCanvas canvas = null;
	
	private GLDrawable surface = null;
	
	private GLContext context = null;
	
	private GL gl = null;
	
	private GLU glu = null;
	
	private GLUT glut = null;
	
	private View currentView = null;
	
	private boolean enabledAllLights = false;
	
	private int lightIdCap = 0;
	
	private int currentLight = 0;
	
	private HashSet<Texture> texturesToAllocate = new HashSet<Texture>();
	
	private HashMap<PointLight, Integer> lightIds = new HashMap<PointLight, Integer>();
	
	private HashMap<Texture, Integer> textureNumberMap = new HashMap<Texture, Integer>();
	
	private SkyBox skyBox = null;
	
	private boolean drawBoundingVolumes = false;
	
	private class GLCanvas extends Canvas {
		
		private static final long serialVersionUID = 1L;
		
		boolean isResized = true;
		
		public void addNotify() {
			super.addNotify();
			surface.setRealized(true);
		}
		
		public void reshape(int w, int h) {
			isResized = true;
			viewportWidth = w;
			viewportHeight = (h == 0) ? 1 : h;
		}
		
	}
	
	public void register(JFrame frame) {
		if (frame == null) {
			throw new IllegalArgumentException();
		}
		
		JPanel panel = new JPanel();
		
		canvas = new GLCanvas();
		
		surface = GLDrawableFactory.getFactory().getGLDrawable(canvas, new GLCapabilities(), null);
		context = surface.createContext(null);
		
		panel.setPreferredSize(frame.getPreferredSize());
		panel.setOpaque(false);
		panel.setLayout(new BorderLayout());
		panel.add(canvas, BorderLayout.CENTER);
		
		panel.addComponentListener(new ComponentAdapter() {
			
			public void componentResized(ComponentEvent evt) {
				Dimension d0 = evt.getComponent().getSize();
				canvas.reshape(d0.width, d0.height);
			}
		});
		
		frame.getContentPane().add(panel);
		
		canvas.setFocusable(false);
		
		frame.pack();
	}
	
	public void initialize() {
		makeContextCurrent();
		
		gl = context.getGL();
		glu = new GLU();
		glut = new GLUT();
		
		resizeView();
		
		gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		
		gl.glEnable(GL.GL_DEPTH_TEST);
		
		gl.glDepthFunc(GL.GL_LEQUAL);
		
		gl.glEnable(GL.GL_NORMALIZE);
		
		gl.glShadeModel(GL.GL_SMOOTH);
		
		// Setting lightning scheme
		{
			gl.glEnable(GL.GL_LIGHTING);
			
			int[] maximumNumberOfLights = new int[1];
			
			gl.glGetIntegerv(GL.GL_MAX_LIGHTS, maximumNumberOfLights, 0);
			
			currentLight = GL.GL_LIGHT0;
			
			lightIdCap = currentLight + maximumNumberOfLights[0];
			
			enabledAllLights = false;
		}
		
		gl.glEnable(GL.GL_TEXTURE_2D);
		
		gl.glEnable(GL.GL_COLOR_MATERIAL);
		
		gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);
		
		context.release();
	}
	
	private void resizeView() {
		gl.glViewport(0, 0, viewportWidth, viewportHeight);
		
		gl.glMatrixMode(GL.GL_PROJECTION);
		
		gl.glLoadIdentity();
		
		if (currentView != null) {
			glu.gluPerspective(currentView.getFov(), getViewportAspect(), currentView.getNear(), currentView.getFar());
		}
	}
	
	private float getViewportAspect() {
		return (float) viewportWidth / (float) viewportHeight;
	}
	
	public void render(View view, GroupNode node) {
		Vector3f eyePosition;
		Vector3f eyeFrontDirection;
		Vector3f eyeUpDirection;
		
		makeContextCurrent();
		
		currentView = view;
		
		if (canvas.isResized) {
			resizeView();
			canvas.isResized = false;
		}
		
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		
		gl.glMatrixMode(GL.GL_MODELVIEW);
		
		gl.glLoadIdentity();
		
		allocateTextures();
		
		eyePosition = currentView.getPosition();
		eyeFrontDirection = new Vector3f(Vector3f.Z_AXIS);
		eyeUpDirection = currentView.getOrientation().getColumn(1);
		
		currentView.getOrientation().multiplyOther(eyeFrontDirection);
		
		gl.glPushMatrix();
		
		gl.glTranslatef(eyePosition.x, eyePosition.y, eyePosition.z);
		
		glu.gluLookAt(eyeFrontDirection.x, eyeFrontDirection.y, eyeFrontDirection.z, 0, 0, 0, eyeUpDirection.x, eyeUpDirection.y, eyeUpDirection.z);
		
		invokeBeforeSceneRenderingCallbacks();
		
		renderSkyBox();
		
		node.onDraw(this);
		
		invokeAfterSceneRenderingCallbacks();
		
		gl.glPopMatrix();
		
		surface.swapBuffers();
		
		context.release();
	}
	
	private void renderSkyBox() {
		if (skyBox != null) {
			gl.glPushMatrix();
			
			Vector3f v0 = currentView.getPosition();
			
			gl.glTranslatef(v0.x, v0.y, v0.z);
			
			gl.glPushAttrib(GL.GL_DEPTH_BUFFER_BIT | GL.GL_LIGHTING_BIT);
			
			gl.glDisable(GL.GL_DEPTH_TEST);
			gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_REPLACE);
			
			// Top
			bindTexture(skyBox.getTop());
			gl.glBegin(GL.GL_QUADS);
			gl.glTexCoord2f(1.0f, 1.0f);
			gl.glVertex3f(-skyBox.getSide(), skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(0.0f, 1.0f);
			gl.glVertex3f(skyBox.getSide(), skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(0.0f, 0.0f);
			gl.glVertex3f(skyBox.getSide(), skyBox.getSide(), skyBox.getSide());
			gl.glTexCoord2f(1.0f, 0.0f);
			gl.glVertex3f(-skyBox.getSide(), skyBox.getSide(), skyBox.getSide());
			gl.glEnd();
			
			// Bottom
			bindTexture(skyBox.getBottom());
			gl.glBegin(GL.GL_QUADS);
			gl.glTexCoord2f(1.0f, 1.0f);
			gl.glVertex3f(skyBox.getSide(), -skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(0.0f, 1.0f);
			gl.glVertex3f(-skyBox.getSide(), -skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(0.0f, 0.0f);
			gl.glVertex3f(-skyBox.getSide(), -skyBox.getSide(), skyBox.getSide());
			gl.glTexCoord2f(1.0f, 0.0f);
			gl.glVertex3f(skyBox.getSide(), -skyBox.getSide(), skyBox.getSide());
			gl.glEnd();
			
			// Front
			bindTexture(skyBox.getFront());
			gl.glBegin(GL.GL_QUADS);
			gl.glTexCoord2f(0.0f, 0.0f);
			gl.glVertex3f(-skyBox.getSide(), -skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(1.0f, 0.0f);
			gl.glVertex3f(skyBox.getSide(), -skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(1.0f, 1.0f);
			gl.glVertex3f(skyBox.getSide(), skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(0.0f, 1.0f);
			gl.glVertex3f(-skyBox.getSide(), skyBox.getSide(), -skyBox.getSide());
			gl.glEnd();
			
			// Back
			bindTexture(skyBox.getBack());
			gl.glBegin(GL.GL_QUADS);
			gl.glTexCoord2f(0.0f, 0.0f);
			gl.glVertex3f(skyBox.getSide(), -skyBox.getSide(), skyBox.getSide());
			gl.glTexCoord2f(1.0f, 0.0f);
			gl.glVertex3f(-skyBox.getSide(), -skyBox.getSide(), skyBox.getSide());
			gl.glTexCoord2f(1.0f, 1.0f);
			gl.glVertex3f(-skyBox.getSide(), skyBox.getSide(), skyBox.getSide());
			gl.glTexCoord2f(0.0f, 1.0f);
			gl.glVertex3f(skyBox.getSide(), skyBox.getSide(), skyBox.getSide());
			gl.glEnd();
			
			// Right
			bindTexture(skyBox.getRight());
			gl.glBegin(GL.GL_QUADS);
			gl.glTexCoord2f(1.0f, 0.0f);
			gl.glVertex3f(skyBox.getSide(), -skyBox.getSide(), skyBox.getSide());
			gl.glTexCoord2f(1.0f, 1.0f);
			gl.glVertex3f(skyBox.getSide(), skyBox.getSide(), skyBox.getSide());
			gl.glTexCoord2f(0.0f, 1.0f);
			gl.glVertex3f(skyBox.getSide(), skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(0.0f, 0.0f);
			gl.glVertex3f(skyBox.getSide(), -skyBox.getSide(), -skyBox.getSide());
			gl.glEnd();
			
			// Left
			bindTexture(skyBox.getLeft());
			gl.glBegin(GL.GL_QUADS);
			gl.glTexCoord2f(1.0f, 0.0f);
			gl.glVertex3f(-skyBox.getSide(), -skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(1.0f, 1.0f);
			gl.glVertex3f(-skyBox.getSide(), skyBox.getSide(), -skyBox.getSide());
			gl.glTexCoord2f(0.0f, 1.0f);
			gl.glVertex3f(-skyBox.getSide(), skyBox.getSide(), skyBox.getSide());
			gl.glTexCoord2f(0.0f, 0.0f);
			gl.glVertex3f(-skyBox.getSide(), -skyBox.getSide(), skyBox.getSide());
			gl.glEnd();
			
			gl.glPopAttrib();
			
			gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE);
			
			gl.glPopMatrix();
		}
	}
	
	private void makeContextCurrent() {
		try {
			while (context.makeCurrent() == GLContext.CONTEXT_NOT_CURRENT) {
				Thread.sleep(100);
			}
		} catch (InterruptedException e) {
		}
	}
	
	public boolean cull(BoundingVolume b0) {
		// TODO:
		return false;
	}
	
	public void draw(PolygonMesh polygonMesh) {
		gl.glPushMatrix();
		
		Transformable transformable = polygonMesh.getLeafNode();
		Vector3f translate = transformable.getPosition();
		gl.glTranslatef(translate.x, translate.y, translate.z);
		
		// TODO: Optimize using quaternions!
		AngleAxis3f orientation = new AngleAxis3f(transformable.getOrientation());
		Vector3f rotate = orientation.getAxis();
		gl.glRotatef((float) Math.toDegrees(orientation.getAngle()), rotate.x, rotate.y, rotate.z);
		
		Vector3f scale = transformable.getScale();
		gl.glScalef(scale.x, scale.y, scale.z);
		
		Material material = polygonMesh.getMaterial();
		if (material != null) {
			bindMaterial(material);
		}
		
		int displayList;
		if (polygonMesh.getUserData() == null) {
			displayList = gl.glGenLists(1);
			gl.glNewList(displayList, GL.GL_COMPILE);
			
			switch (polygonMesh.getMode()) {
			case TRIANGLE_STRIP:
				gl.glBegin(GL.GL_TRIANGLE_STRIP);
				break;
			case TRIANGLES:
				gl.glBegin(GL.GL_TRIANGLES);
				break;
			case POLYGON:
				gl.glBegin(GL.GL_POLYGON);
				break;
			default:
				throw new AssertionError(polygonMesh.getMode());
			}
			
			for (Vertex3f vertex : polygonMesh.getData()) {
				Vector3f normal = vertex.getNormal();
				gl.glNormal3f(normal.x, normal.y, normal.z);
				
				if (material != null && material.isTextureEnabled()) {
					Texture texture = material.getTexture();
					if (texture != null && texture.isRegistered()) {
						Vector2f uvCoordinate = vertex.getUVCoordinate();
						gl.glTexCoord2f(uvCoordinate.x, uvCoordinate.y);
					}
				}
				
				Vector3f position = vertex.getPosition();
				gl.glVertex3f(position.x, position.y, position.z);
			}
			
			gl.glEnd();
			
			gl.glEndList();
			polygonMesh.setUserData(displayList);
		}
		else {
			// FIXME: Unsafe type cast!
			displayList = (Integer) polygonMesh.getUserData();
			gl.glCallList(displayList);
		}
		
		gl.glPopMatrix();
	}
	
	public void draw(PointLight light) {
		int lightId;
		Node node;
		
		if (light.isEnabled())
			return;
		
		if (!lightIds.containsKey(light)) {
			lightId = nextLight();
			
			lightIds.put(light, lightId);
			
			// Components
			gl.glLightfv(lightId, GL.GL_AMBIENT, light.getAmbient().toFloatArray(), 0);
			gl.glLightfv(lightId, GL.GL_DIFFUSE, light.getDiffuse().toFloatArray(), 0);
			gl.glLightfv(lightId, GL.GL_SPECULAR, light.getSpecular().toFloatArray(), 0);
			
			// Position
			node = light.getLeafNode();
			float[] position = new Vector4f(node.getPosition(), 1).toFloatArray();
			gl.glLightfv(lightId, GL.GL_POSITION, position, 0);
			
			// Attenuation
			gl.glLightf(lightId, GL.GL_CONSTANT_ATTENUATION, light.getConstantAttenuationFactor());
			gl.glLightf(lightId, GL.GL_LINEAR_ATTENUATION, light.getLinearAttenuationFactor());
			gl.glLightf(lightId, GL.GL_QUADRATIC_ATTENUATION, light.getQuadraticAttenuationFactor());
			
			if (light.isSpot()) {
				Vector3f direction = new Vector3f(light.getDirection());
				node.getWorldTransform().transform(direction);
				gl.glLightfv(lightId, GL.GL_SPOT_DIRECTION, direction.toFloatArray(), 0);
				
				// Cutoff
				gl.glLightf(lightId, GL.GL_SPOT_CUTOFF, light.getCutoff());
				
				// Concentration
				gl.glLightf(lightId, GL.GL_SPOT_EXPONENT, light.getExponent());
			}
			
			light.setEnabled(true);
		}
	}
	
	public void destroy() {
		context.destroy();
	}
	
	public int nextLight() {
		if (currentLight < lightIdCap) {
			if (!enabledAllLights) {
				gl.glEnable(currentLight);
			}
		} else {
			currentLight = GL.GL_LIGHT0;
			enabledAllLights = true;
		}
		
		return currentLight++;
	}
	
	private void allocateTextures() {
		int[] textureIds;
		int i;
		
		if (!texturesToAllocate.isEmpty()) {
			textureIds = new int[texturesToAllocate.size()];
			gl.glGenTextures(textureIds.length, textureIds, 0);
			
			i = 0;
			for (Texture t0 : texturesToAllocate) {
				textureNumberMap.put(t0, textureIds[i]);
				registerTexture(t0, textureIds[i++]);
			}
			texturesToAllocate.clear();
		}
	}
	
	public void bindTexture(Texture texture) {
		gl.glBindTexture(GL.GL_TEXTURE_2D, textureNumberMap.get(texture).intValue());
	}
	
	public void bindMaterial(Material material) {
		if (material == null) {
			throw new IllegalArgumentException();
		}
		
		// Setting material color
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, material.getAmbient().toFloatArray(), 0);
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_DIFFUSE, material.getDiffuse().toFloatArray(), 0);
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, material.getSpecular().toFloatArray(), 0);
		gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, material.getShininess());
		
		Texture t0 = material.getTexture();
		if (material.isTextureEnabled() && t0 != null && t0.isRegistered()) {
			bindTexture(t0);
		} else {
			unbindTexture();
		}
	}
	
	public void unbindTexture() {
		gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
	}
	
	public void draw(String text, int x, int y) {
		gl.glWindowPos2i(x, y);
		gl.glColor3f(1.0f, 1.0f, 1.0f);
		glut.glutBitmapString(GLUT.BITMAP_9_BY_15, text);
	}
	
	public void draw(Node node) {
		if (node.getWorldBound() != null) {
			drawBoundingSphere((BoundingSphere) node.getWorldBound());
		}
	}
	
	public void draw(SceneObject sceneObject) {
		drawBoundingSphere((BoundingSphere) sceneObject.getBoundingVolume());
	}
	
	private void drawBoundingSphere(BoundingSphere boundingSphere) {
		GLUquadric newQuadric;
		Vector3f c0;
		
		if (!drawBoundingVolumes) {
			return;
		}
		
		newQuadric = glu.gluNewQuadric();
		
		gl.glPushMatrix();
		gl.glPushAttrib(GL.GL_TEXTURE_BIT | GL.GL_LIGHTING_BIT);
		
		gl.glDisable(GL.GL_TEXTURE_2D);
		gl.glDisable(GL.GL_LIGHTING);
		
		c0 = boundingSphere.getCenter();
		
		gl.glTranslatef(c0.x, c0.y, c0.z);
		
		glu.gluQuadricDrawStyle(newQuadric, GLU.GLU_LINE);
		
		glu.gluSphere(newQuadric, boundingSphere.getRadius(), 16, 16);
		
		gl.glPopAttrib();
		gl.glPopMatrix();
		
		glu.gluDeleteQuadric(newQuadric);
	}
	
	private void registerTexture(Texture texture, int textureId) {
		gl.glBindTexture(GL.GL_TEXTURE_2D, textureId);
		
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
		
		int colorMode;
		if (texture.hasAlpha()) {
			colorMode = GL.GL_RGBA;
		}
		else {
			colorMode = GL.GL_RGB;
		}
			
		glu.gluBuild2DMipmaps(GL.GL_TEXTURE_2D, colorMode, texture.getWidth(), texture.getHeight(), colorMode, GL.GL_UNSIGNED_BYTE, texture.getPixels());
		
		texture.setRegistered(true);
	}
	
	public void registerTexture(Texture texture) {
		if (!texture.isRegistered() && !texturesToAllocate.contains(texture)) {
			texturesToAllocate.add(texture);
		}
	}
	
	public void enableBoundingVolumes(boolean enable) {
		drawBoundingVolumes = enable;
	}
	
	public void createSkyBox(String top, String left, String bottom, String right, String front, String back, float size) {
		skyBox = new SkyBox();
		skyBox.setSide(size);
		
		try {
			skyBox.setTop(TextureLoader.readTexture(top));
			skyBox.setLeft(TextureLoader.readTexture(left));
			skyBox.setBottom(TextureLoader.readTexture(bottom));
			skyBox.setRight(TextureLoader.readTexture(right));
			skyBox.setFront(TextureLoader.readTexture(front));
			skyBox.setBack(TextureLoader.readTexture(back));
		} catch (IOException e) {
			throw new RuntimeException("Error creating skybox", e);
		}
		
		registerTexture(skyBox.getTop());
		registerTexture(skyBox.getLeft());
		registerTexture(skyBox.getBottom());
		registerTexture(skyBox.getRight());
		registerTexture(skyBox.getFront());
		registerTexture(skyBox.getBack());
	}
	
}
